package com.matiaspelenur.worldtime;

import android.app.Activity;
import android.app.AlertDialog;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.SharedPreferences;
import android.content.SharedPreferences.Editor;
import android.os.Bundle;
import android.util.TypedValue;
import android.view.KeyEvent;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.BaseExpandableListAdapter;
import android.widget.CheckBox;
import android.widget.CompoundButton;
import android.widget.ExpandableListView;
import android.widget.LinearLayout;
import android.widget.ListView;
import android.widget.TextView;
import android.widget.ViewSwitcher;

import com.google.common.base.Joiner;
import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ListMultimap;
import com.google.common.collect.Lists;
import com.google.common.collect.Sets;

import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.Set;
import java.util.TimeZone;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * Shows the current time at various configurable cities around the world.
 * The full list of regions and cities to choose from is taken from the 
 * {@link TimeZone} class.
 * 
 * See LICENSE file for copyright and licensing.
 * 
 * TODO(matias):
 *  - option to sort by city name?
 *  - filter from list of regions
 *  - short/expanded list of cities
 *  - add additional cities? (e.g. Seattle)
 *  - keyboard search on city list
 *     (replace expanded list with simple view filtered by what's typed)
 *  - add +1 or -1 days from local timezone
 * 
 * @author matias@pelenur.com (Matias Pelenur)
 */
public class WorldTime extends Activity {

  /**
   * Gets notifications of per-minute notifications of time changes, while the app is running.
   */
  private BroadcastReceiver timeTickReceiver;
  
  /**
   * Lists the times at various cities, in the main view
   */
  private ListView mainListView;
  
  /**
   * Lists all the available cities, in a per-region collapsable list.
   */
  private ExpandableListView timeZonesListView;
  
  /**
   * Switches between the two views.
   */
  private ViewSwitcher switcher;
  
  /**
   * Keeps the list of cities that we're showing in the main view.
   */
  private Set<String> enabledCities = Sets.newHashSet();
  
  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    
    mainListView = new ListView(this);
    timeZonesListView = new ExpandableListView(this);
    timeZonesListView.setTextFilterEnabled(true);
    
    switcher = new ViewSwitcher(this);
    switcher.addView(mainListView);
    switcher.addView(timeZonesListView);
    
    setContentView(switcher);
  }
  
  @Override
  public boolean onCreateOptionsMenu(Menu menu) {
    getMenuInflater().inflate(R.menu.options_menu, menu);
    return true;
  }
  
  @Override
  public boolean onOptionsItemSelected(MenuItem item) {
    switch (item.getItemId()) {
      case R.id.choose:
        if (timeZonesListView.getAdapter() == null) {
          timeZonesListView.setAdapter(new RegionCityListAdapter());
        }
        switcher.showNext();
        updateTimes();
        break;
      case R.id.clear:
        maybeClearCities();
        break;
      default:
        throw new RuntimeException("bad menu id");
    }
    return true;
  }

  /**
   * Asks for confirmation, and then clears the enabled cities list.
   */
  private void maybeClearCities() {
    AlertDialog.Builder builder = new AlertDialog.Builder(this);
    builder.setMessage("Are you sure?")
           .setCancelable(false)
           .setPositiveButton("Yes", new DialogInterface.OnClickListener() {
               public void onClick(DialogInterface dialog, int id) {
                   enabledCities.clear();
                   updateTimes();
                   timeZonesListView.setAdapter(new RegionCityListAdapter());
               }
           })
           .setNegativeButton("No", new DialogInterface.OnClickListener() {
               public void onClick(DialogInterface dialog, int id) {
                   dialog.cancel();
               }
           });
    AlertDialog alert = builder.create();
    alert.show();
  }
  
  /**
   * Renames the 'Choose cities' menu item so that it reads "Done" if the user
   * is choosing cities.
   */
  @Override
  public boolean onPrepareOptionsMenu(Menu menu) {
    if (switcher.getNextView() == mainListView) {
      menu.getItem(0).setTitle("Done");
    } else {
      menu.getItem(0).setTitle("Choose cities");
    }
    return true;
  }

  /**
   * Traps the back button, and goes back to the main view if the user is in the 
   * choose cities view.
   */
  @Override
  public boolean onKeyDown(int keyCode, KeyEvent event) {
    if (keyCode == KeyEvent.KEYCODE_BACK) {
      if (switcher.getNextView() == mainListView) {
        switcher.showNext();
        saveToPreferences();
        updateTimes();
        return true;
      }
    }
    return false;
  }

  /**
   * Starts the time tick receiver, and reads any stored preferences.
   * This is called after {@link #onCreate(Bundle)} as well.
   */
  @Override
  protected void onResume() {
    super.onResume();
    
    // Load city list from preferences.
    readFromPreferences();
    
    // Start time ticker.
    timeTickReceiver = new BroadcastReceiver() {
      @Override
      public void onReceive(Context context, Intent intent) {
        updateTimes();
      }
    };
    this.registerReceiver(timeTickReceiver, new IntentFilter(Intent.ACTION_TIME_TICK));
    updateTimes();
  }

  @Override
  protected void onPause() {
    saveToPreferences();
    
    this.unregisterReceiver(timeTickReceiver);
    super.onPause();
  }

  /**
   * Commits the {@link #enabledCities} list preferences.
   */
  private void saveToPreferences() {
    SharedPreferences prefs = getPreferences(MODE_PRIVATE);
    String storedCities = Joiner.on(",").join(enabledCities);
    Editor editor = prefs.edit();
    editor.putString("enabledCities", storedCities);
    editor.commit();
  }

  private void readFromPreferences() {
    SharedPreferences prefs = getPreferences(MODE_PRIVATE);
    String storedCities = prefs.getString("enabledCities", "");
    if (storedCities.length() > 0) {
      enabledCities.clear();
      enabledCities.addAll(Arrays.asList(storedCities.split(",")));
    }
  }

  /**
   * Refreshes the main view of the enabled cities.
   */
  private void updateTimes() {
    List<String> entries;;
    if (enabledCities.isEmpty()) {
      entries = Lists.newArrayList("Press the menu key to choose cities");
    } else {
      entries = getTimes();
    }
    mainListView.setAdapter(new ArrayAdapter<String>(this, R.layout.list_item, entries));
  }

  /**
   * Computes the local time at each of the enabled cities, and sorts them by timezone.
   * @return a List of displayable strings, with the local time and city name.
   */
  private List<String> getTimes() {
    Date now = new Date();
    SimpleDateFormat dateFormat = new SimpleDateFormat("MMDD");
    String date = dateFormat.format(now);
    
    List<NamedTimeZone> timeZones = Lists.newArrayList();
    for (String tzName : enabledCities) {
      TimeZone tz = TimeZone.getTimeZone(tzName);
      if (tz != null) {
        timeZones.add(new NamedTimeZone(tzName, tz));
      }
    }
    Collections.sort(timeZones);
    
    List<String> times = new ArrayList<String>();
    SimpleDateFormat timeformat = new SimpleDateFormat("HH:mm");
    for (NamedTimeZone tz : timeZones) {
      StringBuilder s = new StringBuilder();
      dateFormat.setTimeZone(tz.timeZone);
      timeformat.setTimeZone(tz.timeZone);
      s.append(String.format("%s in %s", timeformat.format(now), tz.getCityName()));
      
      int dateDiff = dateFormat.format(now).compareTo(date);
      if (dateDiff != 0) {
        s.append(String.format(" (%s)", dateDiff == 1 ? "+1" : "-1"));
      }
      
      times.add(s.toString());
    }
    
    return times;
  }
  
  /**
   * Encapsulates a city and its time zone.
   */
  private static class NamedTimeZone implements Comparable<NamedTimeZone> {
    public final String name;
    public final TimeZone timeZone;
    
    private static final Pattern CITY_PATTERN = Pattern.compile("/(\\w+)");
    
    public NamedTimeZone(String name, TimeZone timeZone) {
      this.name = name;
      this.timeZone = timeZone;
    }

    /**
     * @return the "pretty" city name (without region or underscores).
     */
    public String getCityName() {
      Matcher matcher = CITY_PATTERN.matcher(name);
      if (matcher.find()) {
        return matcher.group(1).replace("_", " ");
      }
      return name;
    }
    
    @Override
    public int compareTo(NamedTimeZone that) {
      return this.timeZone.getRawOffset() - that.timeZone.getRawOffset();
    }
  }
  
  /**
   * Implements the expandable list of regions and cities. The region list is currently
   * hard-coded. For each region, all the cities are shown, with checkboxes to enable them.
   * Modifying the checkbox state updates the {@link #enabledCities} set. 
   */
  private class RegionCityListAdapter extends BaseExpandableListAdapter {

    // TODO(matias): let the user enable or disable regions
    private final List<String> enabledRegions = ImmutableList.of(
        "America", "Europe", "Africa", "Asia", "Indian", "Pacific");

    private ListMultimap<String,String> regionCityMap;
    
    public RegionCityListAdapter() {
      populateRegionCityMap();
    }
    
    private void populateRegionCityMap() {
      regionCityMap = ArrayListMultimap.create();
      Pattern tzPattern = Pattern.compile("(\\w+)/(\\w+)");
      
      // TODO(matias): add a "Common" category with the most popular cities.
      // TODO(matias): need to change how the full tz name is computed below, so we can
      // add various region/city combinations here.
      
      for (String tz : TimeZone.getAvailableIDs()) {
        Matcher match = tzPattern.matcher(tz);
        if (match.matches()) {
          String region = match.group(1);
          if (enabledRegions.contains(region)) {
            String city = match.group(2);
            regionCityMap.put(region, city);
          }
        }
      }
    }
    
    @Override
    public Object getChild(int groupPosition, int childPosition) {
      String region = enabledRegions.get(groupPosition);
      String city = regionCityMap.get(region).get(childPosition).replace('_', ' ');
      return city;
    }

    @Override
    public long getChildId(int groupPosition, int childPosition) {
      return childPosition;
    }

    /**
     * Creates the view that renders each list item, i.e. a checkbox and the city name.
     */
    @Override
    public View getChildView(final int groupPosition, final int childPosition, boolean isLastChild,
        View convertView, ViewGroup parent) {
      String region = enabledRegions.get(groupPosition);
      String city = regionCityMap.get(region).get(childPosition);
      final String fullName = region + "/" + city;
      
      LinearLayout layout = new LinearLayout(WorldTime.this);
      TextView textView = new TextView(WorldTime.this);
      textView.setText(getChild(groupPosition, childPosition).toString());
      
      CheckBox checkbox = new CheckBox(WorldTime.this);
      checkbox.setChecked(enabledCities.contains(fullName));
      checkbox.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
        @Override
        public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
          if (isChecked) {
            enabledCities.add(fullName);
          } else {
            enabledCities.remove(fullName);
          }
        }
      });
      layout.addView(checkbox);
      layout.addView(textView);
      return layout;
    }

    @Override
    public int getChildrenCount(int groupPosition) {
      return regionCityMap.get(getGroup(groupPosition).toString()).size();
    }

    @Override
    public Object getGroup(int groupPosition) {
      return enabledRegions.get(groupPosition);
    }

    @Override
    public int getGroupCount() {
      return enabledRegions.size();
    }

    @Override
    public long getGroupId(int groupPosition) {
      return groupPosition;
    }

    @Override
    public View getGroupView(int groupPosition, boolean isExpanded, View convertView,
        ViewGroup parent) {
      TextView textView = new TextView(WorldTime.this);
      textView.setText(getGroup(groupPosition).toString());
      textView.setPadding(50, 10, 0, 10);
      textView.setTextSize(TypedValue.COMPLEX_UNIT_SP, 16.0f);
      return textView;
    }

    @Override
    public boolean hasStableIds() {
      return true;
    }

    @Override
    public boolean isChildSelectable(int groupPosition, int childPosition) {
      return false;
    } 
  }
}
